/*
 * Copyright 2009 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.dailydev.wave.robot;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.dailydev.wave.robot.context.IBundleContext;
import org.dailydev.wave.robot.context.IEventContext;
import org.dailydev.wave.robot.context.impl.BundleContext;
import org.dailydev.wave.robot.filter.IFilter;
import org.dailydev.wave.robot.filter.IFilterManager;

import com.google.wave.api.Capability;
import com.google.wave.api.Context;
import com.google.wave.api.Event;
import com.google.wave.api.EventType;
import com.google.wave.api.RobotMessageBundle;

/**
 * @author Roman Bosak, DailyDev.org
 */
public class Robot implements IRobot {

    /**
     * {@link Robot}'s logger
     */
    private static Logger log = Logger.getLogger(Robot.class.getName());

    /**
     * {@link IRobotHandlerAdapter} to be used to call handler's methods.
     */
    protected IRobotHandlerAdapter robotHandlerAdapter;

    /**
     * Set of robot handlers to dispatch events to.
     */
    protected Set<Object> handlers;

    /**
     * Reference to {@link IFilterManager} which manages list of {@link IFilter}
     * s to be applied before handler method invocation.
     */
    protected IFilterManager filterManager;

    /**
     * Name of the robot as published in robot's profile.
     */
    protected String robotName;

    /**
     * Version of the robot as published in capabilities document. For more
     * details see <a href="http://code.google.com/apis/wave/extensions/robots/java-tutorial.html#Versioning"
     * >Robot Versioning</a>
     */
    protected String robotVersion;

    /**
     * Absolute URL to robot's avatar (eg.
     * http://example-robot.appspot.com/avatar.gif).
     */
    protected String robotAvatar;

    protected Map<EventType, Capability> capabilities;

    protected Map<Method, Object> methodToHandler;

    protected Map<EventType, List<Method>> eventToMethods;

    /**
     * 
     */
    public Robot() {
        this.robotHandlerAdapter = new RobotHandlerAdapter();
    }

    /**
     * {@inheritDoc}
     * 
     * @throws RobotException
     */
    @Override
    public void processEvents(RobotMessageBundle bundle) throws RobotException {
        IBundleContext bundleContext = new BundleContext(bundle);
        List<Event> events = bundle.getEvents();
        for (Event event : events) {
            processEvent(bundle, event, bundleContext.getEventContext(event));
        }
    }

    /**
     * Process a single event from the bundle.
     * 
     * @param bundle
     *            original event bundle
     * @param event
     *            selected event to be processed
     * @param context
     *            context for event handling
     * @throws RobotException
     *             thrown when some problem with event handling occurs
     */
    protected void processEvent(RobotMessageBundle bundle, Event event, IEventContext context) throws RobotException {
        if (log.isLoggable(Level.FINEST)) {
            log.finest("Process event> " + event.getType().name());
        }
        List<Method> handlerMethods = eventToMethods.get(event.getType());
        if (handlerMethods != null && !handlerMethods.isEmpty()) {
            for (Method handlerMethod : handlerMethods) {
                Object handler = methodToHandler.get(handlerMethod);
                if (matchFilters(handler, handlerMethod, bundle, event, context)) {
                    robotHandlerAdapter.handleEvent(handler, handlerMethod, bundle, event, context);
                }
            }
        }
    }

    /**
     * @param handler
     * @param handlerMethod
     * @param bundle
     * @param event
     * @return
     */
    protected boolean matchFilters(Object handler, Method handlerMethod, RobotMessageBundle bundle, Event event,
            IEventContext context) {
        if (filterManager != null) {
            List<IFilter> filters = filterManager.getFiltersToMatch(handler, handlerMethod, event);
            if (log.isLoggable(Level.FINEST)) {
                log.finest("Process filters " + Arrays.toString(filters.toArray()));
            }

            if (filters != null && filters.size() > 0) {
                for (IFilter filter : filters) {
                    if (!filter.filter(handlerMethod, bundle, event, context)) {
                        if (log.isLoggable(Level.FINEST)) {
                            log.finest("Filter '" + filter.getClass().getName() + "' FAILED");
                        }
                        return false;
                    }
                    if (log.isLoggable(Level.FINEST)) {
                        log.finest("Filter '" + filter.getClass().getName() + "' PASSED");
                    }
                }
            }
        }

        return true;
    }

    /**
     * 
     */
    protected void resolveHandlerMethods(Set<Object> handlers) {
        methodToHandler = new HashMap<Method, Object>();
        eventToMethods = new HashMap<EventType, List<Method>>();
        capabilities = new HashMap<EventType, Capability>();

        for (Object handler : handlers) {
            Method[] methods = handler.getClass().getMethods();
            for (Method method : methods) {
                if (method.getAnnotation(EventHandlerOperation.class) != null) {
                    registerMethod(handler, method);
                }
            }
        }
    }

    /**
     * @param handler
     * @param method
     */
    protected void registerMethod(Object handler, Method method) {
        // register method to handler to find it later
        methodToHandler.put(method, handler);

        EventHandlerOperation annotation = method.getAnnotation(EventHandlerOperation.class);
        registerCapabilities(annotation);

        EventType[] eventTypes = annotation.eventTypes();

        // if no eventTypes specified then all EventTypes are used
        if (eventTypes == null || eventTypes.length == 0) {
            eventTypes = EventType.values();
        }

        for (EventType eventType : eventTypes) {
            List<Method> eventMethods = eventToMethods.get(eventType);
            if (eventMethods == null) {
                eventMethods = new ArrayList<Method>();
                eventToMethods.put(eventType, eventMethods);
            }
            eventMethods.add(method);
        }
    }

    /**
     * Registers or updates new capabilities based on {@link EventType}s and
     * {@link Context}s present in annotation.
     * 
     * @param annotation
     *            configures capabilities to be registered
     */
    protected void registerCapabilities(EventHandlerOperation annotation) {
        EventType[] eventTypes = annotation.eventTypes();
        for (EventType eventType : eventTypes) {
            Set<Context> capabilityContexts = new HashSet<Context>();

            Capability existingCapability = capabilities.remove(eventType);
            if (existingCapability != null) {
                List<Context> existingCapabilityContexts = existingCapability.getContexts();
                if (existingCapabilityContexts != null) {
                    capabilityContexts.addAll(existingCapabilityContexts);
                }
            }

            Context[] annotationContexts = annotation.contexts();
            if (annotationContexts != null && annotationContexts.length > 0) {
                capabilityContexts.addAll(Arrays.asList(annotationContexts));
            } else {
                capabilityContexts.addAll(Capability.DEFAULT_CONTEXT);
            }

            Capability capability = new Capability(eventType, new ArrayList<Context>(capabilityContexts));
            capabilities.put(eventType, capability);
        }
    }

    /** {@inheritDoc} */
    public Set<Capability> getCapabilities() {
        return new HashSet<Capability>(capabilities.values());
    }

    /**
     * @return the handlers
     */
    public Set<Object> getHandlers() {
        return handlers;
    }

    /**
     * @param handlers
     *            the handlers to set
     */
    public void setHandlers(Set<Object> handlers) {
        this.handlers = handlers;
        resolveHandlerMethods(this.handlers);
    }

    /** {@inheritDoc} */
    public String getRobotVersion() {
        return robotVersion;
    }

    /**
     * @param robotVersion
     *            the robotVersion to set
     */
    public void setRobotVersion(String robotVersion) {
        this.robotVersion = robotVersion;
    }

    /**
     * @return the robotHandlerAdapter
     */
    public IRobotHandlerAdapter getRobotHandlerAdapter() {
        return robotHandlerAdapter;
    }

    /**
     * @param robotHandlerAdapter
     *            the robotHandlerAdapter to set
     */
    public void setRobotHandlerAdapter(IRobotHandlerAdapter robotHandlerAdapter) {
        this.robotHandlerAdapter = robotHandlerAdapter;
    }

    /**
     * @return the robotName
     */
    public String getRobotName() {
        return robotName;
    }

    /**
     * @param robotName
     *            the robotName to set
     */
    public void setRobotName(String robotName) {
        this.robotName = robotName;
    }

    /**
     * @return the robotAvatar
     */
    public String getRobotAvatar() {
        return robotAvatar;
    }

    /**
     * @param robotAvatar
     *            the robotAvatar to set
     */
    public void setRobotAvatar(String robotAvatar) {
        this.robotAvatar = robotAvatar;
    }

    /**
     * @return the filterManager
     */
    public IFilterManager getFilterManager() {
        return filterManager;
    }

    /**
     * @param filterManager
     *            the filterManager to set
     */
    public void setFilterManager(IFilterManager filterManager) {
        this.filterManager = filterManager;
    }
}
